////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) Microsoft Corporation.  All rights reserved.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
namespace Gadgeteer.SocketInterfaces
{
    using Gadgeteer.Modules;
    using System;
    using System.Collections;
    using System.Threading;
    using Hardware = Microsoft.SPOT.Hardware;

    /// <summary>
    /// Implements the I2C functionality on GPIO pins.
    /// </summary>
    public class SoftwareI2CBus : I2CBus
    {
        private byte address;
        private int clockRateKHz;

        /// <summary>
        /// Gets or sets the address of the <see cref="I2CBus" /> device.
        /// </summary>
        public override ushort Address
        {
            get { return this.address; }
            set { this.address = (byte)value; }
        }

        /// <summary>
        /// Gets or sets the number of milliseconds before a time-out occurs.
        /// </summary>
        public override int Timeout
        {
            get { return timeoutLoopCount * timeoutLoopDelay; }
            set { throw new NotSupportedException(); }
        }

        /// <summary>
        /// Gets or sets the clock speed of the <see cref="I2CBus" /> device.
        /// </summary>
        public override int ClockRateKHz
        {
            get { return clockRateKHz; }
            set { clockRateKHz = value; }
        }

        private static Hashtable ReservedSdaPinPorts = new Hashtable();
        private static Hashtable ReservedSclPinPorts = new Hashtable();

        private DigitalIO sclPort;
        private DigitalIO sdaPort;

        private Socket socket;
        private Socket.Pin sdaPin, sclPin;

        /// <summary>
        /// Defines whether new SoftwareI2C modules will use processor pull ups on the IOs (useful if modules omit the mandatory pull ups). Default is false (modules should provide pull ups).
        /// </summary>
        public static bool ForceManagedPullUps;

        // Note: A constructor summary is auto-generated by the doc builder.
        /// <summary></summary>
        /// <remarks>This automatically checks that the socket supports Type X or Y as appropriate, and reserves the SDA and SCL pins.
        /// An exception will be thrown if there is a problem with these checks.</remarks>
        /// <param name="socket">The socket for this software I2C device interface.</param>
        /// <param name="address">The address of the I2C device.</param>
        /// <param name="clockRateKHz">The maximum clock speed supported by the I2C device.</param>
        /// <param name="sdaPin">The socket pin used for I2C data.</param>
        /// <param name="sclPin">The socket pin used for I2C clock.</param>
        /// <param name="module">The module using this I2C interface, which can be null if unspecified.</param>
        public SoftwareI2CBus(Socket socket, Socket.Pin sdaPin, Socket.Pin sclPin, ushort address, int clockRateKHz, Module module)
        {
            this.address = (byte)address;
            this.clockRateKHz = clockRateKHz;

            // see if we've already reserved the pins and got instances of the ports, otherwise do that.
            string sdaPinString = socket.ToString() + "___" + sdaPin;
            if (!ReservedSdaPinPorts.Contains(sdaPinString))
            {
                sdaPort = DigitalIOFactory.Create(socket, sdaPin, false, GlitchFilterMode.Off, ForceManagedPullUps ? ResistorMode.PullUp : ResistorMode.Disabled, module);
                ReservedSdaPinPorts.Add(sdaPinString, sdaPort);
            }
            else
            {
                sdaPort = (DigitalIO)ReservedSdaPinPorts[sdaPinString];
            }

            string sclPinString = socket.ToString() + "___" + sclPin;
            if (!ReservedSclPinPorts.Contains(sclPinString))
            {
                sclPort = DigitalIOFactory.Create(socket, sclPin, false, GlitchFilterMode.Off, ForceManagedPullUps ? ResistorMode.PullUp : ResistorMode.Disabled, module);
                ReservedSclPinPorts.Add(sclPinString, sclPort);
            }
            else
            {
                sclPort = (DigitalIO)ReservedSclPinPorts[sclPinString];
            }

            this.socket = socket;
            this.sdaPin = sdaPin;
            this.sclPin = sclPin;

            lock (SoftwareI2CTimeoutList)
            {
                timeoutCount = -1;       // Prevent the TimeoutHandler thread from watching this port for now
                SoftwareI2CTimeoutList.Add(this);

                if (timeoutThread == null)
                {
                    threadExit = false;
                    timeoutThread = new Thread(TimeoutHandler);
                    timeoutThread.Start();
                }
            }
        }


        // Variables set to provide a timeout of about 1 second (for managed code implementation)
        private const int timeoutLoopCount = 10;        // 10 loops of
        private const int timeoutLoopDelay = 100;       // 100 mS each

        private static Thread timeoutThread = null;
        private static bool threadExit;
        private int timeoutCount;        // Create this as an array so that it may be locked
        private bool timeout;            // Set true to abort device communications in the event of a timeout
        private static ArrayList SoftwareI2CTimeoutList = new ArrayList();

        /// <summary>
        /// Writes an array of bytes and then reads an array of bytes from/to an I2C device.
        /// </summary>
        /// <param name="writeBuffer">The array of data to write to the device.</param>
        /// <param name="writeOffset">The index of the first byte in the "writeBuffer" array to be written.</param>
        /// <param name="writeLength">The number of bytes from the "writeBuffer" array to be written.</param>
        /// <param name="readBuffer">The array that will hold data read from the device.</param>
        /// <param name="readOffset">The index of the first location in the "readBuffer" array to be written to.</param>
        /// <param name="readLength">The number of bytes that will be written to the "readBuffer" array.</param>
        /// <param name="numWritten">The number of bytes actually written to the device.</param>
        /// <param name="numRead">The number of bytes actually read from the device.</param>
        public override void WriteRead(byte[] writeBuffer, int writeOffset, int writeLength, byte[] readBuffer, int readOffset, int readLength, out int numWritten, out int numRead)
        {
            if (writeBuffer == null)
            {
                writeOffset = writeLength = 0;
            }
            else if (writeBuffer.Length < writeOffset + writeLength || writeOffset < 0 || writeLength < 0)
            {
                throw new ArgumentException("SoftwareI2C: WriteRead call to device at address " + address + " on socket " + socket + " has bad writeBuffer parameters (buffer too small or negative length or offset specified)");
            }

            if (readBuffer == null)
            {
                readOffset = readLength = 0;
            }
            else if (readBuffer.Length < readOffset + readLength || readOffset < 0 || readLength < 0)
            {
                throw new ArgumentException("SoftwareI2C: WriteRead call to device at address " + address + " on socket " + socket + " has bad readBuffer parameters (buffer too small or negative length or offset specified)");
            }

            DoManagedWriteRead(this.address, writeBuffer, writeOffset, writeLength, readBuffer, readOffset, readLength, out numWritten, out numRead);

            if (this.LengthErrorBehavior == ErrorBehavior.ThrowException && (writeLength != numWritten && readLength != numRead))
                throw NewLengthErrorException();
        }

        #region Managed Software I2C Implementation

        private static void TimeoutHandler()
        {
            while (!threadExit)
            {
                try
                {
                    // Perform a timeout check after every timeoutLoopDelay
                    Thread.Sleep(timeoutLoopDelay);

                    lock (SoftwareI2CTimeoutList)
                    {
                        // Check each SoftwareI2C instance for non-responsive communications
                        foreach (SoftwareI2CBus softwarei2c in SoftwareI2CTimeoutList)
                        {
                            if (softwarei2c.timeoutCount >= 0)
                            {
                                softwarei2c.timeoutCount++;
                                if (softwarei2c.timeoutCount >= timeoutLoopCount)
                                {
                                    softwarei2c.timeout = true;              // Abort the current transfer
                                    softwarei2c.timeoutCount = -1;        // This port has failed - no need to keep watching it
                                }
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Microsoft.SPOT.Debug.Print("Exception in SoftwareI2C timeout handling: " + ex);
                }
            }
        }

        // this relies on argument checking already being done in WriteRead
        private void DoManagedWriteRead(byte address, byte[] writeBuffer, int writeOffset, int writeLength, byte[] readBuffer, int readOffset, int readLength, out int numWritten, out int numRead)
        {
            lock (sdaPort)
            {
                lock (SoftwareI2CTimeoutList)
                {
                    timeout = false;
                    timeoutCount = 0;        // Enable timeout checking for this port
                }

                numWritten = 0;
                numRead = 0;

                if (writeLength != 0)
                {
                    // The clock pin should be pulled high - if not, there is a short on the bus
                    // or a nonresponsive device, etc.
                    if (!sclPort.Read()) throw new ApplicationException("Software I2C: clock signal on socket " + socket + " is being held low.");

                    // Generate the start pulse
                    sdaPort.Mode = IOMode.Output;
                    sclPort.Mode = IOMode.Output;

                    // Write the address and data bytes to the device (low order address bit is 0 for write)
                    if (WriteByte((byte)(address << 1)))
                    {
                        for (int index = writeOffset; index < writeOffset + writeLength; index++)
                        {
                            if (!WriteByte(writeBuffer[index])) break;
                            numWritten++;
                        }
                    }

                    if (readLength == 0 || numWritten != writeLength)
                    {
                        // send stop pulse if not reading, or if write failed
                        sclPort.Mode = IOMode.Input;                    // Allow clock pin to float high
                        while (!sclPort.Read() && !timeout) ;                        // Wait for the clock pin to go high
                        sdaPort.Mode = IOMode.Input;                    // Allow data pin to float high
                    }
                    else
                    {
                        // set up for repeated start condition;
                        sdaPort.Mode = IOMode.Input;
                        while (!sdaPort.Read() && !timeout) ;
                        sclPort.Mode = IOMode.Input;
                    }
                }

                if (timeout) throw new ApplicationException("Software I2C: clock signal on socket " + socket + " is being held low.");

                if (numWritten == writeLength && readLength != 0)
                {
                    int limit = (readOffset + readLength) - 1;

                    // The clock pin should be pulled high
                    // If it is not, the bus is shorted or a device is nonresponsive
                    if (!sclPort.Read()) throw new ApplicationException("Software I2C: clock signal on socket " + socket + " is being held low.");

                    // Generate the start pulse
                    sdaPort.Mode = IOMode.Output;
                    sclPort.Mode = IOMode.Output;

                    // Write the address and then read the data bytes from the device (low order address bit is 1 for read)
                    if (WriteByte((byte)((address << 1) | 1)))
                    {
                        int lastIndex = readOffset + readLength - 1;
                        for (int index = readOffset; index < readOffset + readLength; index++)
                        {
                            if (!ReadByte(index == lastIndex, out readBuffer[index])) break;
                            numRead++;
                        }
                    }

                    // Generate the stop pulse
                    sclPort.Mode = IOMode.Input;               // Release the clock line
                    while (!sclPort.Read() & !timeout) ;       // Wait for the clock line to go high
                    sdaPort.Mode = IOMode.Input;               // Release the data line
                }

                if (timeout) throw new ApplicationException("Software I2C: clock signal on socket " + socket + " is being held low.");

                lock (SoftwareI2CTimeoutList)
                {
                    timeoutCount = -1;       // Disable timeout checking for this port
                }
            }

        }

        private bool WriteByte(byte data)
        {
            bool writtenOK = false;
            byte lastBit = 0;            // Assume that the data pin is currently driving low
            byte nextBit;

            // This routine assumes that the clock and data are both driving low
            // Data is transmitted MSB first
            for (int bit = 0; bit < 8; bit++)
            {
                nextBit = (byte)(data & 0x80);
                if (nextBit != lastBit)     // If the state of the data line must change
                {
                    if (nextBit == 0)      // If the data pin must now drive low
                    {
                        sdaPort.Mode = IOMode.Output;
                    }
                    else
                    {
                        sdaPort.Mode = IOMode.Input;
                    }
                    lastBit = nextBit;
                }
                data <<= 1;
                sclPort.Mode = IOMode.Input;            // Let clock line go high
                while (!sclPort.Read() && !timeout) ;   // Wait until the clock line actually goes high
                sclPort.Mode = IOMode.Output;           // Drive the clock back low
            }

            // Check the acknowledge (9th) bit
            if (lastBit == 0)
            {
                sdaPort.Mode = IOMode.Input;   // Change the data pin to an input (stop driving it if it was being driven)
            }
            sclPort.Mode = IOMode.Input;                // Let the clock line go high
            while (!sclPort.Read() && !timeout) ;       // Wait for the clock line to actually go high
            writtenOK = !sdaPort.Read();                // The data line must be driven low by the device to acknowledge the data
            sclPort.Mode = IOMode.Output;               // Drive the clock back low

            // Leave the data pin in an expected state (low)
            sdaPort.Mode = IOMode.Output;

            if (writtenOK && !timeout)
                lock (SoftwareI2CTimeoutList)
                {
                    timeoutCount = 0;                   // We wrote a byte successfully, reset the timouet
                    return true;
                }

            return false;
        }

        private bool ReadByte(bool last, out byte data)
        {
            data = 0;

            // This routine assumes that the clock and data lines are being driven low
            // Data is received MSB first
            sdaPort.Mode = IOMode.Input;                   // Allow the data pin to be driven by the device
            for (int bit = 0; bit < 8; bit++)
            {
                data <<= 1;
                sclPort.Mode = IOMode.Input;               // Let the clock pin go high
                while (!sclPort.Read() && !timeout) ;      // Wait until the device releases the clock pin (if necessary)
                if (sdaPort.Read())
                {
                    data |= 1;
                }
                sclPort.Mode = IOMode.Output;              // Drive the clock pin low again
            }

            // Set the acknowledge bit low for all except the last byte
            if (!last) sdaPort.Mode = IOMode.Output;
            sclPort.Mode = IOMode.Input;                    // Let the clock pin go high
            while (!sclPort.Read() && !timeout) ;           // Wait for the clock pin to actually go high
            sclPort.Mode = IOMode.Output;                   // Drive the clock pin back low
            if (last) sdaPort.Mode = IOMode.Output;         // Drive the data pin low again so that it is in an expected state

            if (!timeout)
                lock (SoftwareI2CTimeoutList)
                {
                    timeoutCount = 0;                  // We read a byte successfully, reset the timeout
                    return true;
                }

            return false;
        }
        
        #endregion
    }
}
